#!/usr/bin/env perl
#
# Copyright (c) 2012-2013 Jeffrey M. Squyres.  All rights reserved.
#
# $COPYRIGHT$
# 
# Additional copyrights may follow
# 
# $HEADER$
#

package RubySlippers::Decision::Time;

use strict;
use RubySlippers::Messages;
use RubySlippers::Globals;
use RubySlippers::Config;
use RubySlippers::Engine;


# Local state
my $_start_time = time();
my $_state = STATE_UNKNOWN;
my $_state_timestamp = time();
my $_timer_name = "Time not home timer";

# Values filled in from the main configuration
my @_devices;
my $_timeout = 15 * 60;
my $_timeout_str;


#
# Set a specific state.  This is only a function because it avoids a
# little code duplication.
#
sub _set_state {
    my $state = shift;
    my $reason = shift;

    if ($state != $_state) {
        $_state = $state;
        $_state_timestamp = time();
    
        RubySlippers::Engine::Act($state, $reason);
    }
}

#
# This function sets a timer: the Query modules may not return any new
# information, and therefore the Decide engine may not get invoked.
# So if timeout seconds elapse, we need to force the Decision engine
# to invoke us to set NOT_HOME.
#
sub _set_not_home_timer {
    # Ensure that there's not an old timer hanging around
    _cancel_not_home_timer();

    my $t = time() + $_timeout;
    Debug("Setting the not_home_timer for $t (time now: " . time() . ")");
    RubySlippers::Engine::add_event(\&_not_home_timer_expired, 
                                    $t,
                                    0, 
                                    "",
                                    $_timer_name);
}

#
# Cancel the not_home timer
#
sub _cancel_not_home_timer {
    Debug("Removing the not_home_timer");
    RubySlippers::Engine::remove_event($_timer_name);
}

#
# This function is called when the not-home timeout has expired and we
# haven't seen any relevant devices.  Hence, we need to set NOT_HOME.
#
sub _not_home_timer_expired {
    Verbose("Time: No relevant devices seen in $_timeout_str");
    _set_state(STATE_NOT_HOME, "No relevant devices seen in $_timeout_str");
}

#
# Check to see if we're home (either just arrived at home, or are
# still home).  If so change state (if necessary), and reset the
# not-home timer.
#
sub _check_for_home {
    my $args = shift;

    my @home_devices;
    foreach my $qd (sort(keys(%{$args->{devices}}))) {
        foreach my $d (@_devices) {
            Debug("Checking for home: $qd vs. $d");
            # We only care about devices that indicate that someone is
            # home
            next
                if ($qd ne $d);

            # A relevant device is home!  Save it.
            push(@home_devices, $d);
        }
    }

    # If we saw home-indicating devices, verboselog it out, set our
    # state to HOME and reset the not-home timer.
    if ($#home_devices >= 0) {
        Debug("Time: Seeing " . join(", ", @home_devices) . 
              " means HOME");
        _set_state(STATE_HOME, "Saw " . join(", ", @home_devices));
        _set_not_home_timer();
    }
}

#
# This function is invoked by the Decide engine when a Query module
# wants to report new information.  We use that new information to
# make a decision.
#
sub _time_decide {
    my $args = shift;

    # We (currently) only look at the output from the MDNS query
    # module (but this can be easily expanded for any other kind of
    # query plugin that looks for devices on a timeout basis).
    if (!defined($args->{query}) || $args->{query} ne "mdns") {
        Debug("Info is not from MDNS; ignoring");
        return;
    }

    # Check to see if we're home (or still home)
    _check_for_home($args);

    # If we're still in the UNKNOWN state and it's been at least
    # timeout seconds since we started, then we should set NOT_HOME.
    if (STATE_UNKNOWN == $_state && time() > $_start_time + $_timeout) {
        _set_state(STATE_NOT_HOME, "Past initial timeout ($_timeout), no relevant devices seen");
    }
}

# Open the module.  We qualify to return a module if we find a list of
# home_devices or not_home_devices and an associated timeout.
#
sub open {
    # If we don't have a list of devices, then don't bother.
    @_devices = 
        RubySlippers::Config::parse_config_list("time_devices", undef);
    if (!defined($_devices[0]) || $#_devices < 0) {
        Verbose("No time_devices defined in config; skipping the Time decision plugin");
        return undef;
    }

    $_timeout = 
        RubySlippers::Config::parse_config_time("time_out", $_timeout);
    $_timeout_str = RubySlippers::Config::make_time_str($_timeout);

    # Set the initial not_home timer so that if no devices are
    # reported by the Query modules, we will set NOT_HOME in timeout
    # seconds.
    _set_not_home_timer();

    # Return 1 module
    Verbose("Time returning Decision module");
    Verbose("    Time monitored devices: " . join(", ", @_devices) . "");
    Verbose("    Time monitor timeout:   $_timeout_str");
    my @ret;
    push(@ret, {
        decide => \&_time_decide,
        name => "Time decide",
         });
    return \@ret;
}

1;
